use std::path::Path;
use tracing::info;

use bc_base::{TransactionId, transaction::Transaction};
use bc_db::DBError;

use crate::inner_storage::StorageBase;

#[derive(Debug)]
pub(crate) struct TransactionStorage {
    tx_db: StorageBase<TransactionId, Transaction>,
}

impl TransactionStorage {
    pub(crate) fn new(db_root_dir: &Path) -> Result<Self, DBError> {
        let path = crate::get_transactions_db_path(db_root_dir);
        info!(
            ">>>> The path of TransactionStorage DB: {}",
            path.to_str().unwrap()
        );
        Ok(Self {
            tx_db: StorageBase::new(&path)?,
        })
    }

    pub(crate) async fn put(
        &self,
        key: &TransactionId,
        value: &Transaction,
    ) -> Result<(), DBError> {
        self.tx_db.put(key, value).await
    }

    pub(crate) async fn get(&self, key: &TransactionId) -> Result<Option<Transaction>, DBError> {
        self.tx_db.get(key).await
    }

    pub(crate) async fn erase(&self, key: &TransactionId) -> Result<(), DBError> {
        self.tx_db.erase(key).await
    }

    pub(crate) async fn exists(&self, key: &TransactionId) -> bool {
        self.tx_db.exists(key).await
    }

    pub(crate) async fn flush_database(&self) -> Result<(), DBError> {
        self.tx_db.flush_database().await
    }

    #[cfg(debug_assertions)]
    pub(crate) async fn get_item_number(&self) -> usize {
        self.tx_db.get_item_number().await
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use scopeguard::defer;

    use bc_base::{TransactionValue, test_utils::test_tx};

    #[tokio::test]
    async fn test_transaction_storage() {
        let tmp_dir = bc_util::convert_to_temp_dir("test_transaction_storage");
        let path = tmp_dir.path().to_path_buf();
        defer! {
            tmp_dir.close().unwrap();
        };

        let storage = TransactionStorage::new(&path).unwrap();
        let tx = test_tx::new_tx_simple(100);
        let tx_id = tx.calc_tx_id();

        assert!(!(storage.exists(&tx_id).await));
        assert!(storage.put(&tx_id, &tx).await.is_ok());
        assert!(storage.exists(&tx_id).await);
        assert_eq!(storage.get(&tx_id).await.unwrap().unwrap(), tx);
        assert!(storage.erase(&tx_id).await.is_ok());
        assert!(!(storage.exists(&tx_id).await));
    }

    async fn add_tx_add_check(
        storage: &TransactionStorage,
        value: TransactionValue,
    ) -> TransactionId {
        let tx = test_tx::new_tx_simple(value);
        let tx_id = tx.calc_tx_id();

        assert!(!(storage.exists(&tx_id).await));
        assert!(storage.put(&tx_id, &tx).await.is_ok());
        assert!(storage.exists(&tx_id).await);

        tx_id
    }

    #[tokio::test]
    async fn test_transaction_storage_exists() {
        let tmp_dir = bc_util::convert_to_temp_dir("test_transaction_storage_exists");
        let path = tmp_dir.path().to_path_buf();
        defer! {
            tmp_dir.close().unwrap();
        };

        let storage = TransactionStorage::new(&path).unwrap();

        let tx_id_1 = add_tx_add_check(&storage, 100).await;
        let tx_id_2 = add_tx_add_check(&storage, 200).await;
        let tx_id_3 = add_tx_add_check(&storage, 300).await;

        assert!(storage.exists(&tx_id_1).await);
        assert!(storage.exists(&tx_id_2).await);
        assert!(storage.exists(&tx_id_3).await);
    }

    // new, put, flush, exist
    #[ignore = "todo: fix exists bug"]
    #[tokio::test]
    async fn test_transaction_storage_put_flush_exist() {
        let tmp_dir = bc_util::convert_to_temp_dir("test_transaction_storage_put_flush_exist");
        let path = tmp_dir.path().to_path_buf();
        defer! {
            tmp_dir.close().unwrap();
        };

        let storage = TransactionStorage::new(&path).unwrap();

        let tx_1 = test_tx::new_tx_simple(100);
        let tx_1_id = tx_1.calc_tx_id();
        let tx_2 = test_tx::new_tx_simple(101);
        let tx_2_id = tx_2.calc_tx_id();
        assert_ne!(&tx_1_id, &tx_2_id);

        assert!(storage.put(&tx_1_id, &tx_1).await.is_ok());
        assert!(!storage.exists(&tx_2_id).await);
        assert!(storage.flush_database().await.is_ok());
        assert!(!storage.exists(&tx_2_id).await);
    }
}
